package com.munchlevel.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Toast;

import com.gandulf.guilib.util.Debug;
import com.munchlevel.BaseGameActivity;
import com.munchlevel.R;

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for incoming
 * connections, a thread for connecting with a device, and a thread for
 * performing data transmissions when connected.
 */
public class BluetoothService {

	// Key names received from the BluetoothChatService Handler
	public static final String DEVICE_NAME = "device_name";
	public static final String DEVICE_ADDRESS = "device_address";
	public static final String TOAST = "toast";

	// Message types sent from the BluetoothChatService Handler
	public static final int MESSAGE_STATE_CHANGE = 1;
	public static final int COMMAND_READ = 2;
	public static final int COMMAND_WRITE = 3;
	public static final int MESSAGE_TOAST = 5;
	public static final int MESSAGE_CONNECTION_FAILED = 6;

	public static final String COMMAND_END = ";";
	public static final String COMMAND_SEP = ":";

	private BaseGameActivity gameActivity;

	// Name for the SDP record when creating server socket
	private static final String NAME = "MunchLevel";

	// Unique UUID for this application
	private static final UUID MY_UUID = UUID.fromString("fb77c0d0-afac-11de-8a39-0800200c9a66");

	// Member fields
	private final BluetoothAdapter mAdapter;

	private AcceptThread mAcceptThread;
	private ConnectThread mConnectThread;
	private ConnectedThread mConnectedThread;

	private int mState;

	/**
	 * Constants that indicate the current connection state: we're doing nothing
	 */
	public static final int STATE_NONE = 0;

	/**
	 * Constants that indicate the current connection state: now listening for
	 * incoming connections
	 */
	public static final int STATE_LISTEN = 1;

	/**
	 * Constants that indicate the current connection state: now initiating an
	 * outgoing connection
	 */
	public static final int STATE_CONNECTING = 2;

	/**
	 * Constants that indicate the current connection state: now connected to a
	 * remote device
	 */
	public static final int STATE_CONNECTED = 3;

	/**
	 * Constants that indicate the current connection state: Name of the
	 * connected device
	 */
	protected String mConnectedDeviceName = null;
	protected String mConnectedDeviceAddress = null;

	private BluetoothServiceListener bluetoothServiceListener;

	public interface BluetoothServiceListener {

		public void onConnected(String deviceName, String address);

		public boolean onCommandReceived(String cmd, String params);

		public void onConnectFailed(String name, String address);
	}

	// The Handler that gets information back from the BluetoothChatService
	private final Handler mHandler = new Handler() {

		@Override
		public void handleMessage(Message msg) {

			String cmd;
			switch (msg.what) {
			case BluetoothService.MESSAGE_STATE_CHANGE:
				Debug.verbose("BS: state change from " + msg.arg2 + " to " + msg.arg1);
				switch (msg.arg1) {
				case BluetoothService.STATE_CONNECTED:
					Debug.verbose("BS: Connected with:" + mConnectedDeviceName);
					if (bluetoothServiceListener != null)
						bluetoothServiceListener.onConnected(mConnectedDeviceName, mConnectedDeviceAddress);

					break;
				case BluetoothService.STATE_CONNECTING:
					// mTitle.setText(R.string.title_connecting);
					break;
				case BluetoothService.STATE_LISTEN:
				case BluetoothService.STATE_NONE:
					// mTitle.setText(R.string.title_not_connected);
					break;
				}
				break;
			case BluetoothService.MESSAGE_CONNECTION_FAILED:
				String name = msg.getData().getString(BluetoothService.DEVICE_NAME);
				String address = msg.getData().getString(BluetoothService.DEVICE_ADDRESS);
				if (bluetoothServiceListener != null)
					bluetoothServiceListener.onConnectFailed(name, address);

			case BluetoothService.COMMAND_WRITE:
				cmd = (String) msg.obj;
				break;
			case BluetoothService.COMMAND_READ:
				cmd = (String) msg.obj;

				String params = null;
				if (cmd.indexOf(COMMAND_SEP) >= 0) {
					params = cmd.substring(cmd.indexOf(COMMAND_SEP) + 1);
					cmd = cmd.substring(0, cmd.indexOf(COMMAND_SEP));
				}
				Debug.verbose("BS: Parsed msg " + msg.obj + ", command:" + cmd + ",params:" + params);

				if (bluetoothServiceListener != null)
					bluetoothServiceListener.onCommandReceived(cmd, params);
				break;
			case BluetoothService.MESSAGE_TOAST:
				Toast.makeText(gameActivity, msg.getData().getString(BluetoothService.TOAST), Toast.LENGTH_SHORT)
						.show();
				break;
			}
		}
	};

	/**
	 * Constructor. Prepares a new BluetoothChat session.
	 * 
	 * @param context
	 *            The UI Activity Context
	 */
	public BluetoothService(BaseGameActivity context) {
		this.gameActivity = context;
		mAdapter = BluetoothAdapter.getDefaultAdapter();
		mState = STATE_NONE;
	}

	public boolean connect(String address) {
		BluetoothDevice device = mAdapter.getRemoteDevice(address);
		connect(device);
		return true;
	}

	public String getRemoteAddress() {
		return mConnectedDeviceAddress;
	}

	public String getRemoteName() {
		return mConnectedDeviceName;
	}

	public BluetoothServiceListener getBluetoothServiceListener() {
		return bluetoothServiceListener;
	}

	public void setBluetoothServiceListener(BluetoothServiceListener bluetoothServiceListener) {
		this.bluetoothServiceListener = bluetoothServiceListener;
	}

	/**
	 * Set the current state of the chat connection
	 * 
	 * @param state
	 *            An integer defining the current connection state
	 */
	private synchronized void setState(int state) {

		int oldState = mState;
		mState = state;

		// Give the new state to the Handler so the UI Activity can update
		mHandler.obtainMessage(MESSAGE_STATE_CHANGE, state, oldState).sendToTarget();
	}

	/**
	 * Return the current connection state.
	 */
	public synchronized int getState() {
		return mState;
	}

	public void ensureDiscoverable() {
		if (mAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
			Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
			discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
			gameActivity.startActivity(discoverableIntent);
		}
	}

	public Set<BluetoothDevice> getBondedDevices() {
		return mAdapter.getBondedDevices();
	}

	public boolean cancelDiscovery() {
		return mAdapter.cancelDiscovery();
	}

	public boolean startDiscovery() {
		// If we're already discovering, stop it
		if (mAdapter.isDiscovering()) {
			mAdapter.cancelDiscovery();
		}

		// Request discover from BluetoothAdapter
		return mAdapter.startDiscovery();
	}

	/**
	 * Start the chat service. Specifically start AcceptThread to begin a
	 * session in listening (server) mode. Called by the Activity onResume()
	 */
	public synchronized void start() {
		Debug.verbose("BS: start");

		// Cancel any thread attempting to make a connection
		if (mConnectThread != null) {
			mConnectThread.cancel();
			mConnectThread = null;
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Start the thread to listen on a BluetoothServerSocket
		if (mAcceptThread == null) {
			mAcceptThread = new AcceptThread();
			mAcceptThread.start();
		}
		setState(STATE_LISTEN);
	}

	/**
	 * Start the ConnectThread to initiate a connection to a remote device.
	 * 
	 * @param device
	 *            The BluetoothDevice to connect
	 */
	public synchronized void connect(BluetoothDevice device) {
		Debug.verbose("BS: connect to: " + device);

		// Cancel any thread attempting to make a connection
		if (mState == STATE_CONNECTING) {
			if (mConnectThread != null) {
				mConnectThread.cancel();
				mConnectThread = null;
			}
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Start the thread to connect with the given device
		mConnectThread = new ConnectThread(device);
		mConnectThread.start();
		setState(STATE_CONNECTING);
	}

	/**
	 * Start the ConnectedThread to begin managing a Bluetooth connection
	 * 
	 * @param socket
	 *            The BluetoothSocket on which the connection was made
	 * @param device
	 *            The BluetoothDevice that has been connected
	 */
	public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
		Debug.verbose("BS: Connected with " + device.getName());

		// Cancel the thread that completed the connection
		if (mConnectThread != null) {
			mConnectThread.cancel();
			mConnectThread = null;
		}

		// Cancel any thread currently running a connection
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}

		// Cancel the accept thread because we only want to connect to one
		// device
		if (mAcceptThread != null) {
			mAcceptThread.cancel();
			mAcceptThread = null;
		}

		// Start the thread to manage the connection and perform transmissions
		mConnectedThread = new ConnectedThread(socket);
		mConnectedThread.start();

		// Set the name of the connected device
		mConnectedDeviceName = device.getName();
		mConnectedDeviceAddress = device.getAddress();

		setState(STATE_CONNECTED);
	}

	/**
	 * Stop all threads
	 */
	public synchronized void stop() {
		Debug.verbose("BS: stop");

		if (mConnectThread != null) {
			mConnectThread.cancel();
			mConnectThread = null;
		}
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
			mConnectedThread = null;
		}
		if (mAcceptThread != null) {
			mAcceptThread.cancel();
			mAcceptThread = null;
		}
		setState(STATE_NONE);
	}

	public void restart() {
		stop();
		start();
	}

	public void sendCommand(String cmd, String params) {
		sendCommand(cmd + COMMAND_SEP + params);
	}

	/**
	 * Write to the ConnectedThread in an unsynchronized manner
	 * 
	 * @param out
	 *            The bytes to write
	 * @see ConnectedThread#write(byte[])
	 */
	public void sendCommand(String cmd) {

		// Check that there's actually something to send
		if (cmd != null && cmd.length() > 0) {

			if (getState() != BluetoothService.STATE_CONNECTED) {
				sendToastHandle(gameActivity.getString(R.string.not_connected));
				return;
			}

			// Get the message bytes and tell the BluetoothChatService to write
			if (!cmd.endsWith(BluetoothService.COMMAND_END))
				cmd += BluetoothService.COMMAND_END;

			byte[] out = cmd.getBytes();

			// Create temporary object
			ConnectedThread r;
			// Synchronize a copy of the ConnectedThread
			synchronized (this) {
				if (mState != STATE_CONNECTED)
					return;
				r = mConnectedThread;
			}
			// Perform the write unsynchronized
			r.write(out);
		}
	}

	/**
	 * Indicate that the connection attempt failed and notify the UI Activity.
	 */

	private void connectionFailed(BluetoothDevice device) {
		connectionFailed(device.getName(), device.getAddress());
	}

	private void connectionFailed(String name, String address) {
		// Start the service over to restart listening mode
		Message msg = mHandler.obtainMessage(MESSAGE_CONNECTION_FAILED);
		Bundle bundle = new Bundle();
		bundle.putString(DEVICE_NAME, name);
		bundle.putString(DEVICE_ADDRESS, address);
		msg.setData(bundle);
		mHandler.sendMessage(msg);

		start();
	}

	private void sendToastHandle(String msg) {
		Message messsage = mHandler.obtainMessage(MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(TOAST, msg);
		messsage.setData(bundle);
		mHandler.sendMessage(messsage);
	}

	/**
	 * Indicate that the connection was lost and notify the UI Activity.
	 */
	private void connectionLost() {
		start();
	}

	/**
	 * This thread runs while listening for incoming connections. It behaves
	 * like a server-side client. It runs until a connection is accepted (or
	 * until cancelled).
	 */
	private class AcceptThread extends Thread {
		// The local server socket
		private final BluetoothServerSocket mmServerSocket;

		private boolean canceled = false;

		public AcceptThread() {
			BluetoothServerSocket tmp = null;

			// Create a new listening server socket
			try {
				tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
			} catch (IOException e) {
				Debug.error("BS: listen() failed", e);
			}
			mmServerSocket = tmp;
		}

		public void run() {
			// constructor threw IOException
			if (mmServerSocket == null) {
				return;
			}
			Debug.verbose("BS: BEGIN mAcceptThread" + this);
			setName("AcceptThread");

			BluetoothSocket socket = null;

			// Listen to the server socket if we're not connected
			while (mState != STATE_CONNECTED) {
				try {
					// This is a blocking call and will only return on a
					// successful connection or an exception
					socket = mmServerSocket.accept();
				} catch (IOException e) {
					if (!canceled)
						Debug.error("BS: accept() failed", e);
					break;
				}

				// If a connection was accepted
				if (socket != null) {
					synchronized (BluetoothService.this) {
						switch (mState) {
						case STATE_LISTEN:
						case STATE_CONNECTING:
							// Situation normal. Start the connected thread.
							connected(socket, socket.getRemoteDevice());
							break;
						case STATE_NONE:
						case STATE_CONNECTED:
							// Either not ready or already connected. Terminate
							// new socket.
							try {
								socket.close();
							} catch (IOException e) {
								Debug.error("BS: Could not close unwanted socket", e);
							}
							break;
						}
					}
				}
			}
			Debug.verbose("BS: END mAcceptThread");
		}

		public void cancel() {
			canceled = true;
			try {
				if (mmServerSocket != null)
					mmServerSocket.close();
			} catch (IOException e) {
				Debug.error("BS: close() of server failed", e);
			}
		}
	}

	/**
	 * This thread runs while attempting to make an outgoing connection with a
	 * device. It runs straight through; the connection either succeeds or
	 * fails.
	 */
	private class ConnectThread extends Thread {
		private BluetoothSocket mmSocket = null;
		private final BluetoothDevice mmDevice;

		private boolean canceled = false;

		public ConnectThread(BluetoothDevice device) {
			mmDevice = device;
		}

		public void run() {
			Debug.verbose("BS: BEGIN mConnectThread");
			setName("ConnectThread");

			// Always cancel discovery because it will slow down a connection
			mAdapter.cancelDiscovery();

			// Get a BluetoothSocket for a connection with the given
			// BluetoothDevice
			try {
				mmSocket = mmDevice.createRfcommSocketToServiceRecord(MY_UUID);
				// This is a blocking call and will only return on a
				// successful connection or an exception
				mmSocket.connect();
			} catch (IOException e) {
				if (!canceled) {
					try {
						Method m = mmDevice.getClass().getMethod("createRfcommSocket", new Class[] { int.class });
						mmSocket = (BluetoothSocket) m.invoke(mmDevice, 1);
						// This is a blocking call and will only return on a
						// successful connection or an exception
						mmSocket.connect();
					} catch (Exception e1) {
						connectionFailed(mmDevice);

						if (!canceled) {
							// Send a failure message back to the Activity
							sendToastHandle("Unable to connect device " + mmDevice.getName());
							Debug.error("BS: create() failed", e1);
						}
						try {
							// Close the socket
							if (mmSocket != null)
								mmSocket.close();
						} catch (IOException e2) {
							Debug.error("BS: unable to close() socket during connection failure", e2);
						}
						return;
					}
				}

			}

			// Reset the ConnectThread because we're done
			synchronized (BluetoothService.this) {
				mConnectThread = null;
			}

			if (mmSocket != null) {
				// Start the connected thread
				connected(mmSocket, mmDevice);
			}
		}

		public void cancel() {
			canceled = true;
			try {
				if (mmSocket != null) {
					mmSocket.close();
					mmSocket = null;
				}
			} catch (IOException e) {
				Debug.error("BS: close() of connect socket failed", e);
			}
		}
	}

	/**
	 * This thread runs during a connection with a remote device. It handles all
	 * incoming and outgoing transmissions.
	 */
	private class ConnectedThread extends Thread {
		private final BluetoothSocket mmSocket;
		private final InputStream mmInStream;
		private final OutputStream mmOutStream;

		private boolean canceled = false;

		public ConnectedThread(BluetoothSocket socket) {
			Debug.verbose("BS: create ConnectedThread");
			mmSocket = socket;
			InputStream tmpIn = null;
			OutputStream tmpOut = null;

			// Get the BluetoothSocket input and output streams
			try {
				tmpIn = socket.getInputStream();
				tmpOut = socket.getOutputStream();
			} catch (IOException e) {
				Debug.error("BS: temp sockets not created", e);
			}

			mmInStream = tmpIn;
			mmOutStream = tmpOut;
		}

		public void run() {
			// constructor threw exception
			if (mmInStream == null || mmOutStream == null) {
				return;
			}
			Debug.verbose("BS: BEGIN mConnectedThread");
			byte[] buffer = new byte[1024];
			int bytes;

			StringBuilder sb = new StringBuilder();
			String commands = null;
			String token = null;
			boolean skipLast;
			// Keep listening to the InputStream while connected
			while (true) {
				try {
					if (canceled)
						return;

					// Read from the InputStream
					bytes = mmInStream.read(buffer);
					sb.append(new String(buffer, 0, bytes));

					if (sb.indexOf(COMMAND_END) >= 0) {
						commands = sb.toString();
						StringTokenizer st = new StringTokenizer(commands, COMMAND_END, false);
						sb.setLength(0);

						skipLast = !commands.endsWith(COMMAND_END);
						while (st.hasMoreTokens()) {
							token = st.nextToken();
							if (!st.hasMoreTokens() && skipLast) {
								sb.append(token);
								break;
							} else {
								// Send the obtained commands to the UI Activity
								mHandler.obtainMessage(COMMAND_READ, bytes, -1, token).sendToTarget();
							}
						}
					}
				} catch (IOException e) {
					// if this thread was canceled the error probably occured
					// because the socket was closed.
					if (!canceled) {
						sendToastHandle("Device connection was lost");
						Debug.error("BS: disconnected", e);
					}
					connectionLost();
					break;
				}
			}
		}

		/**
		 * Write to the connected OutStream.
		 * 
		 * @param buffer
		 *            The bytes to write
		 */
		public void write(byte[] buffer) {
			try {
				mmOutStream.write(buffer);

				// Share the sent message back to the UI Activity
				mHandler.obtainMessage(COMMAND_WRITE, -1, -1, new String(buffer)).sendToTarget();
			} catch (IOException e) {
				Debug.error("BS: Exception during write", e);
			}
		}

		public void cancel() {
			canceled = true;
			try {

				if (mmSocket != null) {
					mmSocket.close();
				}
			} catch (IOException e) {
				Debug.error("BS: close() of connect socket failed", e);
			}
		}
	}

}
